package com.nulldev.util.net.packets;

import com.nulldev.util.FinalBypass;
import com.nulldev.util.crypto.encryptAPI.litestring.LiteString;
import com.nulldev.util.encoding.compression.smartCompression;
import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.other.ByteUtils;
import com.nulldev.util.string.StringsUtil;

public abstract class NetPacket {
	
	/**
	 * Replaced by {@link com.nulldev.util.net.packets.APILevel} <br>
	 * Replacement reason: To much work to update the value 'SINCE_API' and manually implement.
	 */
	@Deprecated
	private int SINCE_API = 1;
	
	@Deprecated
	/**
	 * Replaced by PacketMode enum.
	 */
	private boolean shouldCompress;
	
	public void client(final String data, final String raw, final NetInfo client) {
		this.client(data.getBytes(), raw.getBytes(), client);
	}
	
	public void client(final byte[] data, final String raw, final NetInfo client) {
		this.client(data, raw.getBytes(), client);
	}
	
	public abstract void client(final byte[] data, final byte[] raw, final NetInfo client);
	public abstract void server(final byte[] data, final byte[] raw, final NetInfo connection);
	
	public void server(final String data, final String raw, final NetInfo connection) {
		this.server(data.getBytes(), raw.getBytes(), connection);
	}
	
	public void server(final byte[] data, final String raw, final NetInfo connection) {
		this.server(data, raw.getBytes(), connection);
	}
	
	private final String id;
	private final String packetName;
	private byte[] data;
	private PacketType type;
	
	public NetPacket(final String packetName, final String id, final String data) {
		this(packetName, id, data.getBytes(), PacketType.UNIVERSAL);
	}
	
	public NetPacket(final String packetName, final String id, final byte[] data) {
		this(packetName, id, data, PacketType.UNIVERSAL);
	}
	
	public NetPacket(final String packetName, final String id, final String data, final PacketType type) {
		this(packetName, id, data.getBytes(), type);
	}
	
	public NetPacket(final String packetName, final String id, final byte[] data, final PacketType type) {
		this.id = id;
		this.data = data;
		this.packetName = packetName;
		this.type = type;
	}
	
	/**
	 * Marked for partial deprecation due to breaking with large sizes. [64KB+]
	 */
	@Deprecated
	public boolean shouldCompress() {
		return this.shouldCompress;
	}
	
	/**
	 * Marked for partial deprecation due to breaking with large sizes. [64KB+]
	 * @param enabled
	 */
	@Deprecated
	public void setCompressionEnabled(final boolean enabled) {
		this.shouldCompress = enabled;
	}
	
	@Deprecated
	/**
	 * Replaced by {@link com.nulldev.util.net.packets.APILevel} <br>
	 * Replacement reason: To much work to update the value 'SINCE_API' and manually implement.
	 */
	public int getAPIMin() {
		LoggerFactory.getLogger(true, true).warn("getAPIMin() is deprecated. You should be using APILevel.");
		return this.SINCE_API;
	}
	
	@Deprecated
	/**
	 * Replaced by {@link com.nulldev.util.net.packets.APILevel} <br>
	 * Replacement reason: To much work to update the value 'SINCE_API' and manually implement.
	 */
	public void setAPIMin(final int level) {
		LoggerFactory.getLogger(true, true).warn("setAPIMin() is deprecated. You should be using APILevel.");
		this.SINCE_API = level;
	}
	
	public int getAPILevel() {
		if (this.getClass().isAnnotationPresent(APILevel.class)) {
			final APILevel api = this.getClass().getAnnotation(APILevel.class);
			if (api != null) {
				return api.level();
			}else {
				return -1;
			}
		}
		return -1;
	}
	
	public String getPacketName() {
		return this.packetName;
	}
	
	public String getID() {
		return this.id;
	}
	
	public byte[] getData() {
		return this.data;
	}
	
	public void changeData(final byte[] data) {
		this.data = data;
	}
	
	public PacketType type() {
		return this.type;
	}
	
	public boolean isClient() {
		return this.type == PacketType.CLIENT;
	}
	
	public boolean isServer() {
		return this.type == PacketType.SERVER;
	}
	
	public boolean isUniversal() {
		return this.type == PacketType.UNIVERSAL;
	}
	
	@Override
	/**
	 * toString() defaults to toString(PacketMode.STANDARD_V2);
	 * @returns Sendable Packet
	 */
	public String toString() {
		return toString(PacketMode.STANDARD_V2, null);
//		if (this.shouldCompress) {
//			return "__p2c[" + this.id + "]" + LiteString.zipUTF8_Base64(this.data);
//		}else {
//			return "__p2[" + this.id + "]" + this.data;
//		}
	}
	
	/**
	 * Creates a packet ready to send <br>
	 * <b>WARNING</b>: STANDARD_V2_FULLY_ENCRYPTED is unsupported! 
	 * @param mode
	 * @param netinfo [Used for STANDARD_V2_ENCRYPTED, can null if not needed]
	 * @returns Sendable Packet
	 */
	public String toString(final PacketMode mode, final NetInfo netinfo) {
		if (mode == PacketMode.LEGACY_V1) {
			return "__p2[" + this.id + "]" + createString(this.data);
		}else if (mode == PacketMode.LEGACY_V1_COMPRESSED) {
			return "__p2c[" + this.id + "]" + LiteString.zipUTF8_Base64B(this.data);
		}else if (mode == PacketMode.STANDARD_V2) {
			return "[NP3{" + this.id + (this.shouldCompress ? ",COMP" : "") + "}]" + createString(this.data);
		}else if (mode == PacketMode.STANDARD_V2_ENCRYPTED) {
			if (netinfo != null) {
				return netinfo.attemptEncryptionV3(this);
			}
		}
		return null;
	}
	
	public String getHeader() {
		return this.getHeader(PacketMode.defaultMode);
	}
	
	public byte[] autoReadData(final PacketMode mode) {
		return this.autoReadData(PacketMode.defaultMode, null);
	}
	
	public byte[] autoReadData(final PacketMode mode, final NetInfo info) {
		if (mode == PacketMode.LEGACY_V1_COMPRESSED) {
			return LiteString.zipUTF8(data);
		}else if (mode == PacketMode.STANDARD_V2_COMPRESSED) {
			return this.data;
		}else if (mode == PacketMode.STANDARD_V2_ENCRYPTED && info != null) {
			return info.attemptEncryptionV3Bytes(this);
		}else if (mode == PacketMode.STANDARD_V2_FULLY_ENCRYPTED && info != null) {
			return info.attemptEncryptionV3Bytes(this);
		}else {
			return this.data;
		}
	}
	
	public String getHeader(final PacketMode mode) {
		return this.getHeader(mode, null);
	}
	
	public String getHeader(final PacketMode mode, final NetInfo info) {
		if (mode == PacketMode.LEGACY_V1) {
			return "__p2[" + this.id + "]";
		}else if (mode == PacketMode.LEGACY_V1_COMPRESSED) {
			return "__p2c[" + this.id + "]";
		}else if (mode == PacketMode.STANDARD_V2) {
			return "[NP3{" + this.id + "}]";
		}else if (mode == PacketMode.STANDARD_V2_COMPRESSED) {
			return "[NP3{" + this.id + ",COMP_SC}]";
		}else if (mode == PacketMode.STANDARD_V2_ENCRYPTED) {
			return "[NP3{" + this.id + ",ENC_NCRYPT}]";
		}else if (mode == PacketMode.STANDARD_V2_FULLY_ENCRYPTED && info != null) {
			return "[NP3E]" + ByteUtils.toBase64(info.attemptEncryptionV3Bytes(
					this.getHeader(PacketMode.STANDARD_V2)));
		}
		return null;
	}
	
//	public String createV2Packet() {
//		return "[NP3{" + this.id + (this.shouldCompress ? ",COMP" : "") + "}]" + createString(this.data);
//	}
	
	@Override
	public Object clone() {
		return new NetPacket(this.packetName, this.id, this.data, this.type) {
			
			@Override
			public void server(byte[] data, byte[] raw, final NetInfo connection) {
				NetPacket.this.server(data, raw, connection);
			}
			
			@Override
			public void client(byte[] data, byte[] raw, final NetInfo client) {
				NetPacket.this.client(data, raw, client);
			}
		};
		
	}
	
	public boolean isGeneric() {
		return this == PacketManager.GENERIC || this.equals(PacketManager.PARTIAL) || this.getID().equals(PacketManager.PARTIAL.getID());
	}
	
	public String substringBetween(final String data, final String open, final String close) {
		return StringsUtil.substringBetween(data, open, close);
	}
	
	@Override
	public boolean equals(final Object obj) {
		if (obj instanceof NetPacket) {
			final NetPacket packet = (NetPacket) obj;
			return packet.getID().equals(this.getID()) && packet.getData().equals(this.getData());
		}
        return (this == obj);
    }
	
	public String createString(final byte[] bytes) {
		return new String(bytes);
	}
	
	private boolean preferBytePayload;
	
	public boolean preferBytePayload() {
		return this.preferBytePayload;
	}
	
	public void preferBytePayload(final boolean enable) {
		this.preferBytePayload = enable;
	}
}
